﻿// sktoolslib - common files for SK tools

// Copyright (C) 2012, 2017-2018 - Stefan Kueng

// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software Foundation,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//

#include "stdafx.h"
#include <Shlwapi.h>
#include "DirFileEnum.h"

#pragma comment(lib, "shlwapi.lib")

CSimpleFileFind::CSimpleFileFind(const std::wstring &sPath, LPCTSTR pPattern, FINDEX_INFO_LEVELS infoLevel)
    : m_dError(ERROR_SUCCESS)
    , m_bFirst(true)
    , m_sPathPrefix(sPath)
{
    m_FindFileData = {};
    if (PathIsDirectory(sPath.c_str()))
    {
        // Add a trailing \ to m_sPathPrefix if it is missing.
        // Do not add one to "C:" since "C:" and "C:\" are different.
        {
            int len = (int)m_sPathPrefix.size();
            if (len != 0) {
                TCHAR ch = sPath[len - 1];
                if (ch != '\\' && (ch != ':' || len != 2)) {
                    m_sPathPrefix += '\\';
                }
            }
        }
        m_hFindFile = ::FindFirstFileEx(std::wstring(m_sPathPrefix + pPattern).c_str(), infoLevel, &m_FindFileData, FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH);
        m_bFile = FALSE;
    }
    else
    {
        m_hFindFile = ::FindFirstFile(m_sPathPrefix.c_str(), &m_FindFileData);
        m_bFile = TRUE;
    }
    if (m_hFindFile == INVALID_HANDLE_VALUE) {
        m_dError = ::GetLastError();
    }
}

CSimpleFileFind::~CSimpleFileFind()
{
    if (m_hFindFile != INVALID_HANDLE_VALUE) {
        ::FindClose(m_hFindFile);
    }
}

bool CSimpleFileFind::FindNextFile()
{
    if (m_dError) {
        return false;
    }

    if (m_bFirst) {
        m_bFirst = false;
        return (m_hFindFile != INVALID_HANDLE_VALUE);
    }

    if (!::FindNextFile(m_hFindFile, &m_FindFileData)) {
        m_dError = ::GetLastError();
        return false;
    }

    return true;
}

bool CSimpleFileFind::FindNextFileNoDots(DWORD attrToIgnore)
{
    bool result;
    do {
        result = FindNextFile();
    } while (result && (IsDots() || ((GetAttributes() & attrToIgnore) != 0)));

    return result;
}

bool CSimpleFileFind::FindNextFileNoDirectories()
{
    bool result;
    do {
        result = FindNextFile();
    } while (result && IsDirectory());

    return result;
}


/*
* Implementation notes:
*
* This is a depth-first traversal.  Directories are visited before
* their contents.
*
* We keep a stack of directories.  The deepest directory is at the top
* of the stack, the originally-requested directory is at the bottom.
* If we come across a directory, we first return it to the user, then
* recurse into it.  The finder at the bottom of the stack always points
* to the file or directory last returned to the user (except immediately
* after creation, when the finder points to the first valid thing we need
* to return, but we haven't actually returned anything yet - hence the
* m_bIsNew variable).
*
* Errors reading a directory are assumed to be end-of-directory, and
* are otherwise ignored.
*
* The "." and ".." psedo-directories are ignored for obvious reasons.
*/


CDirFileEnum::CDirStackEntry::CDirStackEntry(CDirStackEntry * seNext, const std::wstring& sDirName)
    : CSimpleFileFind(sDirName)
    , m_seNext(seNext)
{
}

CDirFileEnum::CDirStackEntry::~CDirStackEntry()
{
}

inline void CDirFileEnum::PopStack()
{
    CDirStackEntry * seToDelete = m_seStack;
    m_seStack = seToDelete->m_seNext;
    delete seToDelete;
}

inline void CDirFileEnum::PushStack(const std::wstring& sDirName)
{
    m_seStack = new CDirStackEntry(m_seStack, sDirName);
}

CDirFileEnum::CDirFileEnum(const std::wstring& sDirName) :
    m_seStack(nullptr),
    m_bIsNew(true),
    m_attrToIgnore(0)
{
    PushStack(sDirName);
}

CDirFileEnum::~CDirFileEnum()
{
    while (m_seStack != nullptr) {
        PopStack();
    }
}

bool CDirFileEnum::NextFile(std::wstring &sResult, bool* pbIsDirectory, bool recurse)
{
    if (m_bIsNew) {
        // Special-case first time - haven't found anything yet,
        // so don't do recurse-into-directory check.
        m_bIsNew = false;
    } else if (m_seStack && m_seStack->IsDirectory() && recurse && ((m_seStack->GetAttributes() & m_attrToIgnore)==0)) {
        PushStack(m_seStack->GetFilePath());
    }

    while (m_seStack && !m_seStack->FindNextFileNoDots(m_attrToIgnore)) {
        // No more files in this directory, try parent.
        PopStack();
    }

    if (m_seStack)
    {
        sResult = m_seStack->GetFilePath();
        if (pbIsDirectory != nullptr)
        {
            *pbIsDirectory = m_seStack->IsDirectory();
        }
        return true;
    } else {
        return false;
    }
}
